package cn.yangliu.config;

import cn.yangliu.constant.ResultCode;
import cn.yangliu.ex.ToolException;
import cn.yangliu.model.Result;
import lombok.extern.slf4j.Slf4j;
import org.apache.logging.log4j.util.Strings;
import org.springframework.beans.ConversionNotSupportedException;
import org.springframework.beans.TypeMismatchException;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingPathVariableException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.async.AsyncRequestTimeoutException;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.multipart.support.MissingServletRequestPartException;
import org.springframework.web.servlet.NoHandlerFoundException;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.UndeclaredThrowableException;
import java.util.Objects;

/**
 * the type of serial-port-tool
 *
 * <p>
 * .
 *
 * @author 杨柳
 * @date 2024-05-29
 */
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandlerImpl {

    /**
     * Constructor.
     */
    public GlobalExceptionHandlerImpl() {
        log.info("Use default GlobalExceptionHandler implement!");
    }

    /**
     * Ascii Result.
     *
     * @param e the e
     * @param request the request
     * @return the result
     */
    @ExceptionHandler(ToolException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Result<Object> handleToolException(ToolException e,
                                               HttpServletRequest request) {
        log.error(e.getMsg(), e);
        return new Result<>(e);
    }

    /**
     * Provides handling for standard Spring MVC exceptions.
     *
     * @param e the target exception
     * @param request the current request
     * @param response the response
     * @return the response entity
     */
    @ExceptionHandler({ HttpRequestMethodNotSupportedException.class,
            HttpMediaTypeNotSupportedException.class,
            HttpMediaTypeNotAcceptableException.class,
            MissingPathVariableException.class,
            MissingServletRequestParameterException.class,
            ServletRequestBindingException.class,
            ConversionNotSupportedException.class, TypeMismatchException.class,
            HttpMessageNotReadableException.class,
            HttpMessageNotWritableException.class,
            MethodArgumentNotValidException.class,
            MissingServletRequestPartException.class, BindException.class,
            NoHandlerFoundException.class, AsyncRequestTimeoutException.class,
            MethodArgumentTypeMismatchException.class, Exception.class })
    public ResponseEntity<Object> handleException(Exception e,
                                                  HttpServletRequest request, HttpServletResponse response) {
        log.error(e.getMessage(), e);

        HttpHeaders headers = new HttpHeaders();
        /**
         * if current exception's cause is an instance of ToolException
         *
         * try to get ToolException from current exception's cause
         * and get message from ToolException
         *
         * if not , continue check
         */
        Throwable cause = e.getCause();
        if (Objects.nonNull(cause) && (cause instanceof ToolException)) {
            Result<Object> result = new Result<>((ToolException) cause);
            return new ResponseEntity<>(result, headers, HttpStatus.BAD_REQUEST);
        }

        if (e instanceof UndeclaredThrowableException) {
            UndeclaredThrowableException ute = (UndeclaredThrowableException) e;
            Throwable throwable = ute.getUndeclaredThrowable();
            if (Objects.nonNull(throwable) && (throwable instanceof InvocationTargetException)) {
                InvocationTargetException ite = (InvocationTargetException) throwable;
                Throwable targetException = ite.getTargetException();
                if (Objects.nonNull(targetException) && (targetException instanceof ToolException)) {
                    Result<Object> result = new Result<>((ToolException) targetException);
                    return new ResponseEntity<>(result, headers, HttpStatus.BAD_REQUEST);
                }
            }
        }

        HttpStatus status = null;
        ResultCode resultCode = null;
        /**
         * Handle particular exceptions below
         */
        if (e instanceof HttpRequestMethodNotSupportedException) {
            status = HttpStatus.METHOD_NOT_ALLOWED;
            resultCode = resultCode.METHOD_NOT_ALLOWED;
        } else if (e instanceof HttpMediaTypeNotSupportedException) {
            status = HttpStatus.UNSUPPORTED_MEDIA_TYPE;
            resultCode = resultCode.UNSUPPORTED_MEDIA_TYPE;
        } else if (e instanceof HttpMediaTypeNotAcceptableException) {
            status = HttpStatus.NOT_ACCEPTABLE;
            resultCode = resultCode.UNSUPPORTED_MEDIA_TYPE;
        } else if (e instanceof MissingPathVariableException
                || e instanceof ConversionNotSupportedException
                || e instanceof HttpMessageNotWritableException) {
            status = HttpStatus.INTERNAL_SERVER_ERROR;
            resultCode = resultCode.PARAM_IS_INVALID;
        } else if (e instanceof MissingServletRequestParameterException) {
            status = HttpStatus.BAD_REQUEST;
            resultCode = resultCode.PARAM_IS_BLANK;
        } else if (e instanceof ServletRequestBindingException
                || e instanceof HttpMessageNotReadableException
                || e instanceof MethodArgumentTypeMismatchException) {
            status = HttpStatus.BAD_REQUEST;
            resultCode = resultCode.PARAM_IS_INVALID;
        } else if (e instanceof TypeMismatchException) {
            status = HttpStatus.BAD_REQUEST;
            resultCode = resultCode.PARAM_TYPE_BIND_ERROR;
        } else if (e instanceof BindException) {
            status = HttpStatus.BAD_REQUEST;
            resultCode = resultCode.PARAM_TYPE_BIND_ERROR;
            e = new ToolException(resultCode, handleBindingResult(
                    ((BindException) e).getBindingResult()));
        } else if (e instanceof MethodArgumentNotValidException) {
            status = HttpStatus.BAD_REQUEST;
            resultCode = resultCode.PARAM_TYPE_BIND_ERROR;
            e = new ToolException(resultCode,handleBindingResult(
                    ((MethodArgumentNotValidException) e).getBindingResult()));
        } else if (e instanceof MissingServletRequestPartException) {
            status = HttpStatus.BAD_REQUEST;
            resultCode = resultCode.INTERFACE_ADDRESS_NOT_FOUND;
        } else if (e instanceof NoHandlerFoundException) {
            status = HttpStatus.NOT_FOUND;
            resultCode = resultCode.INTERFACE_ADDRESS_NOT_FOUND;
        } else if (e instanceof AsyncRequestTimeoutException) {
            status = HttpStatus.SERVICE_UNAVAILABLE;
            resultCode = resultCode.INTERFACE_REQUEST_TIMEOUT;
        } else {
            status = HttpStatus.INTERNAL_SERVER_ERROR;
            resultCode = httpStatusToresultCode(status);
        }
        if (e instanceof ToolException) {
            Result<Object> result = new Result<>((ToolException) e);
            return new ResponseEntity<>(result,headers, status);
        }
        return new ResponseEntity<>(new Result<>(resultCode), headers, status);
    }

    private ResultCode httpStatusToresultCode(HttpStatus status) {
        ResultCode resultCode = null;
        if (status.equals(HttpStatus.NOT_FOUND)) {
            resultCode = ResultCode.INTERFACE_ADDRESS_NOT_FOUND;
        } else if (status.equals(HttpStatus.UNAUTHORIZED)) {
            resultCode = ResultCode.UNAUTHORIZED;
        } else if (status.equals(HttpStatus.FORBIDDEN)) {
            resultCode = ResultCode.ACCESS_DENIED;
        } else if (status.is4xxClientError()) {
            resultCode = ResultCode.BAD_REQUEST;
        } else {
            resultCode = ResultCode.SYSTEM_INNER_ERROR;
        }
        return resultCode;
    }


    protected String handleBindingResult(BindingResult result) {
        String message = ResultCode.PARAM_TYPE_BIND_ERROR.getMsg();
        if (!result.hasErrors()) {
            return message;
        }
        /**
         * DefaultMessage in AllErrors list splitting by ','
         */
        return Strings.join(result.getAllErrors().stream()
                .map(ObjectError::getDefaultMessage).iterator(), ',');
    }

}
